package network;

import jdd.bdd.BDD;
import model.BDDEngine;
import model.Model;

import java.util.*;

public class Network {
    private Model model;
    private ArrayList<Rule> initialRules;
    private HashMap<String, Device> nameToDevice;

    /**
     * A Network starts with a set of devices without any rule and a set of initial rules.
     * Before using the network, it must initialize its model and rules: it first binds
     * with a unique model and then add all initial rules into network and its model
     * at the same time.
     *
     * It worth noting a Network always has a 'default' device.
     */
    public Network() {
        this.nameToDevice = new HashMap<>();
        this.initialRules = new ArrayList<>();
    }

    /**
     * Notice the clone takes an empty initialRules.
     * @return an instance of network with another model
     */
    public Network clone() {
        Network ret = new Network();
        for (Map.Entry<String, Device> devEntry : nameToDevice.entrySet()) {
            ret.nameToDevice.put(devEntry.getKey(), devEntry.getValue().clone());
        }
        return ret;
    }

    public Model getModel() {
        return this.model;
    }

    public void addDevice(String name) {
        Device device = new Device(name);
        this.nameToDevice.put(name, device);
    }

    public void addLink(String fromd, String fromp, String tod, String top) {
        Device fromdev = this.nameToDevice.get(fromd);
        Device todev = this.nameToDevice.get(tod);
        Port frompt = fromdev.getPortByName(fromp);
        Port topt = todev.getPortByName(top);
        if (frompt == null) {
            frompt = fromdev.addPortByName(fromp);
        }
        if (topt == null) {
            topt = todev.addPortByName(top);
        }
        frompt.addPeer(topt);
        topt.addPeer(frompt);
    }

    public void addInitialRules(Rule rule) {
        this.initialRules.add(rule);
    }

    public ArrayList<Rule> getInitialRules() {
        return this.initialRules;
    }

    public HashSet<Device> getDevicesWithoutDefault() {
        HashSet<Device> ret = new HashSet<>();
        for(Map.Entry<String, Device> entry : this.nameToDevice.entrySet())
            if (!entry.getKey().contentEquals("default")) {
                ret.add(entry.getValue());
            }
        return ret;
    }

    public HashMap<String, Device> getNameToDevice() {
        return nameToDevice;
    }

    public void initializeModelAndRules(long dstIp, int priority) {
        this.model = new Model();
        ArrayList<Port> ports = new ArrayList<>();
        for (int i = 0; i < Device.cnt; i ++) ports.add(null);
        // Initialize each device with default rule sending packets to default device (i.e., black-hole)
        for (Device device : this.getDevicesWithoutDefault()) {
            Port defaultPort = device.addPortByName(device.getName() + "-default");
            Rule rule = new Rule(device, dstIp, priority, defaultPort);
            this.addRule(rule);
            // initializing default rules w/o generating changes
            ports.set(device.uid, defaultPort);
        }
        Ports key = new Ports(ports);
        model.portsToPred.put(key, BDDEngine.BDDTrue);
    }

    public void addRule(Rule rule) {
        Changes changes = new Changes(model.bddEngine);
        this.identifyChangesInsert(rule, changes);
    }

    public void insertMiniBatch(ArrayList<Rule> rules, Changes changes) {
        Comparator<Rule> comp = (Rule lhs, Rule rhs) -> {
            // -1 - less than, 1 - greater than, 0 - equal, all inversed for descending
            if (lhs.getPriority() > rhs.getPriority()) return -1;
            if (lhs.getPriority() < rhs.getPriority()) return 1;

            if (lhs.getDstIp() > rhs.getDstIp()) return -1;
            if (lhs.getDstIp() < rhs.getDstIp()) return 1;

            if (lhs.getPort().hashCode() > rhs.getPort().hashCode()) return -1;
            if (lhs.getPort().hashCode() < rhs.getPort().hashCode()) return 1;

            if (lhs.getDevice().hashCode() ==  lhs.getDevice().hashCode()) return 0;
            if (lhs.getDevice().hashCode() <  lhs.getDevice().hashCode()) {
                return -1;
            } else {
                return 1;
            }
        };
        Collections.sort(rules, comp);
        // WARNING: the delta of a mini-batch is based on the very initial state,
        // i.e., change of any rule in the batch is not allowed;
        // Although sorting in descending order of priority can achieve the goal,
        // this may not be the best implementation.

        for (Rule rule : rules) {
            identifyChangesInsert(rule, changes);
        }
    }

    public void identifyChangesInsert(Rule rule, Changes changes) {
        BDD bdd = this.model.bddEngine.bdd;
        int entryBdd = this.model.bddEngine.encodeDstIPPrefix(rule.getDstIp(), rule.getPriority());
        rule.setNMatch(bdd.ref(bdd.not(entryBdd)));
        rule.setHit(entryBdd);

        Device device = rule.getDevice();
        for (Rule r : device.addAndGetAllUntil(rule)) {
            if (r.getPriority() > rule.getPriority()) {
                int newHit = bdd.ref(bdd.and(rule.getHit(), r.getNMatch()));

                bdd.deref(rule.getHit());
                rule.setHit(newHit);
            }

            if (rule.getHit() == BDDEngine.BDDFalse) return;

            if (r.getPriority() < rule.getPriority()) {
                int intersection = bdd.ref(bdd.and(r.getHit(), rule.getHit()));
                if (r.getPort() != rule.getPort()) changes.add(intersection, r.getPort(), rule.getPort());

                int tmp = bdd.ref(bdd.not(intersection));
                int newHit = bdd.ref(bdd.and(r.getHit(), tmp));
                bdd.deref(tmp);
                bdd.deref(intersection);

                bdd.deref(r.getHit());
                r.setHit(newHit);
            }
        }
    }
}
